package NES::PPU;

use strict;
use Data::Dumper;
use NES::PPU::NameTable;
use NES::PPU::PatternTable;
use NES::PPU::AttributeTable;
use Utils qw|test_bit|;

use constant {
    # scanlines
    PRE_RENDER_SCANLINE   => -1,
    LAST_VISIBLE_SCANLINE => 239,
    POST_RENDER_SCANLINE  => 240,
    LAST_VBLANK_SCANLINE  => 260,
    LAST_SCANLINE         => 261,

    # interrupts
    NMI => 1,
};

sub new {
    my $class = shift;

    my $self = bless {
        open_bus        => 0,
        vram_buffer     => 0,
        latch           => 1,
        reg             => { 0 => 0, 1 => 0, 2 => 0, 3 => 0, 4 => 0, 5 => 0, 6 => 0, 7 => 0 },
        name_table      => [ map { NES::PPU::NameTable->new } (1..4) ],
        pattern_table   => [ map { NES::PPU::PatternTable->new } (1..2) ],
        attribute_table => [ map { NES::PPU::AttributeTable->new } (1..4) ],
        read_vram       => undef,
        write_vram      => undef,
        scanline        => -1,
        vblank          => 0,
        nmi             => 0,
        render_buffer   => [],
        aquire_screen   => undef,
        mirroring       => undef,
        render_buffer   => [],
        @_,
    }, $class;

    return $self;
}

sub read_vram {
    my $self        = shift;
    my ( $address ) = @_;

    return $self->{read_vram}( $address );
}

sub write_vram {
    my $self                = shift;
    my ( $address, $value ) = @_;

    return $self->{write_vram}( $address, $value );
}

sub read {
    my $self        = shift;
    my ( $address ) = @_;

    my $reg = ( $address & 0x0007 );

    if( grep { $reg == $_ } qw|0 1 3 4 6| ) { # FIXME: read a 2005?
        return $self->{open_bus};
    } else {
        if( $reg == 2 ) {
            return $self->read_2002;
        } elsif( $reg == 7 ) {
            return $self->read_2007;
        } else {
            return $self->{reg}{ $reg };
        }
    }
}

sub write {
    my $self                = shift;
    my ( $address, $value ) = @_;

    my $reg = ( $address & 0x0007 );
    $value  = ( $value & 0x00ff );

    if( grep { $reg == $_ } qw|0 1 3 4 5 6 7| ) {
        if( $reg == 5 ) {
            $self->write_2005( $value );
        } elsif( $reg == 6 ) {
            $self->write_2006( $value );
        } elsif( $reg == 7 ) {
            $self->write_2007( $value );
        } else {
            $self->{reg}{ $reg } = $value;
        }
    } else {
        $self->{open_bus} = $value;
    }
}

sub read_2002 {
    my $self = shift;

    my $temp = $self->{reg}{2};

    # vblank
    $temp = ( ($temp | ($self->{vblank} << 7)) & 0x00ff );

    $self->{reg}{2} = ( $self->{reg}{2} & 0x007f );
    $self->{latch}  = 1;

    return $temp;
}

sub write_2005 {
    my $self      = shift;
    my ( $value ) = @_;

    $self->{latch} = ( $self->{latch} == 1 ? 0 : 1 );
}

sub write_2006 {
    my $self      = shift;
    my ( $value ) = @_;

    if( $self->{latch} ) {
        $self->{reg}{6} = ( ($value << 8) & 0xff00 );
        $self->{latch} = 0;
    } else {
        # FIXME es correcto usar 0x3fff, o deberia ser 0xffff? segun 'http://wiki.nesdev.com/w/index.php/PPU_registers#Data_.28.242007.29_.3C.3E_read.2Fwrite', las
        # direcciones superiores a (0x0000..0x3fff) son mirrors de esta
        $self->{reg}{6} = ( ($self->{reg}{6} | $value) & 0x3fff );
        $self->{latch} = 1;
    }
}

sub read_2007 {
    my $self = shift;

    my $return = undef;

    # la direccion no corresponde a la paleta
    if( $self->{reg}{6} < 0x3f00 ) {
        $return = $self->{vram_buffer};
        $self->{vram_buffer} = $self->read_vram( $self->{reg}{6} );
    }

    # la direccion corresponde a la paleta
    elsif( $self->{reg}{6} < 0x4000 ) {
        $return = $self->read_vram( $self->{reg}{6} );
        $self->{vram_buffer} = $self->read_vram( ($self->{reg}{6} - 0x1000) & 0xffff );
    }

    my $increment   = ( test_bit($self->{reg}{0}, 2) ? 32 : 1 );
    $self->{reg}{6} = ( ($self->{reg}{6} + $increment) & 0xffff );

    return $return;
}

sub write_2007 {
    my $self      = shift;
    my ( $value ) = @_;

    # name tables
    if( $self->{reg}{6} < 0x3000 ) {
        # FIXME: mappers?
        if( $self->{reg}{6} >= 0x2000 ) {
            my $address = ( $self->{reg}{6} & 0x2c00 );

            # mirroing vertical
            if( $self->{mirroring} == 1 ) {
                if( $address == 0x2000 ) {
                    $self->write_vram( $self->{reg}{6}, $value );   $self->write_vram( ($self->{reg}{6} + 0x0800), $value );
                } elsif( $address == 0x2400 ) {
                    $self->write_vram( $self->{reg}{6}, $value );   $self->write_vram( ($self->{reg}{6} + 0x0800), $value );
                } elsif( $address == 0x2800 ) {
                    $self->write_vram( $self->{reg}{6}, $value );   $self->write_vram( ($self->{reg}{6} - 0x0800), $value );
                } elsif( $address == 0x2c00 ) {
                    $self->write_vram( $self->{reg}{6}, $value );   $self->write_vram( ($self->{reg}{6} - 0x0800), $value );
                }
            }

            # mirroring horizontal
            elsif( $self->{mirroring} == 0 ) {
                if( $address == 0x2000 ) {
                    $self->write_vram( $self->{reg}{6}, $value );   $self->write_vram( ($self->{reg}{6} + 0x0400), $value );
                } elsif( $address == 0x2400 ) {
                    $self->write_vram( $self->{reg}{6}, $value );   $self->write_vram( ($self->{reg}{6} - 0x0400), $value );
                } elsif( $address == 0x2800 ) {
                    $self->write_vram( $self->{reg}{6}, $value );   $self->write_vram( ($self->{reg}{6} + 0x0400), $value );
                } elsif( $address == 0x2c00 ) {
                    $self->write_vram( $self->{reg}{6}, $value );   $self->write_vram( ($self->{reg}{6} - 0x0400), $value );
                }
            }
        }
    }

    # paleta
    # FIXME: mirroring?
    elsif( $self->{reg}{6} < 0x4000 ) {
        $value = ( $value & 0x003f );
        $self->write_vram( $self->{reg}{6}, $value );
    }

    my $increment   = ( test_bit($self->{reg}{0}, 2) ? 32 : 1 );
    $self->{reg}{6} = ( ($self->{reg}{6} + $increment) & 0xffff );
}

sub run_scanline {
    my $self = shift;

    if( $self->{scanline} <= PRE_RENDER_SCANLINE ) {
        # no se renderiza nada
        # $self->dump_pattern_tables_ascii_mode;
        # $self->dump_pattern_tables;
    } elsif( $self->{scanline} <= LAST_VISIBLE_SCANLINE ) {
        # renderizar bkg y spr
        $self->draw_background;
    } elsif( $self->{scanline} <= POST_RENDER_SCANLINE ) {
        # el vblank comienza al final de este scanline
        $self->render;
        $self->{vblank} = 1;
        $self->throw_interrupt( NMI ) if( test_bit($self->{reg}{0}, 7) );
    } elsif( $self->{scanline} <= LAST_VBLANK_SCANLINE ) {
        # periodo de vblank
    } elsif( $self->{scanline} <= LAST_SCANLINE ) {
        # fin del periodo de vblank
        $self->{vblank}   = 0;
        $self->{scanline} = -2; # para que al sumarle 1 pase a -1, o sea el pre-render scanline
    }

    $self->{scanline}++;
}

sub draw_background {
    my $self = shift;

    # FIXME
    my $y_displacement = ( $self->{scanline} % 8 );

    my $pt_address = ( (test_bit($self->{reg}{0}, 4) << 12) & 0xffff );
    my $nt_address = ( ((test_bit($self->{reg}{0}, 0) << 10) | (test_bit($self->{reg}{0}, 1) << 11) | (1 << 13)) & 0xffff );

    # FIXME
    my $palette = {
        0 => 0xff0000ff,
        1 => 0x00ff00ff,
        2 => 0x0000ffff,
        3 => 0xff00ffff,
    };

    for( my $tile = 0; $tile < 32; $tile++ ) {
        my $nt_entry = $self->read_vram( ($nt_address + $tile + (($self->{scanline} >> 3) << 5)) & 0xffff );
        my $pt_entry = {
            l => $self->read_vram( ($pt_address + $nt_entry * 16 + $y_displacement) & 0x3fff ),
            h => $self->read_vram( ($pt_address + $nt_entry * 16 + $y_displacement + 8) & 0x3fff ),
        };

        for( my $bit = 7; $bit >= 0; $bit-- ) {
            my $index = ( (($pt_entry->{l} >> $bit) & 0x0001) | ((($pt_entry->{h} >> $bit) & 0x0001) << 1) & 0x0003 );
            push @{ $self->{render_buffer} }, $palette->{ $index };
        }
    }
}

sub render {
    my $self = shift;

    my $surface = SDLx::Surface->new( width => 256, height => 240 );
    SDL::Video::lock_surface( $surface );

    for( my $y = 0; $y < 30 * 8; $y++ ) {
        for( my $x = 0; $x < 32 * 8; $x++ ) {
            $surface->[$x][$y] = shift @{ $self->{render_buffer} };
        }
    }

    SDL::Video::unlock_surface( $surface );
    my $screen = $self->aquire_screen;
    SDL::Video::MUSTLOCK( $screen );
    $surface->blit( $screen );
    SDL::Video::unlock_surface( $screen );
    $screen->update;
}

sub aquire_screen {
    my $self = shift;

    return $self->{aquire_screen}();
}

sub throw_interrupt {
    my $self          = shift;
    my ( $interrupt ) = @_;

    $self->{nmi} = 1 if( $interrupt == NMI );
}

sub want_nmi {
    my $self = shift;

    return $self->{nmi};
}

sub toggle_nmi {
    my $self = shift;

    $self->{nmi} = 0;
}

sub dump_pattern_tables {
    my $self = shift;

    my $pt0    = { start => 0x0000, end => 0x1000 };
    my $pt1    = { start => 0x1000, end => 0x2000 };

    # FIXME
    my $palette = {
        0 => 0xff0000ff,
        1 => 0x00ff00ff,
        2 => 0x0000ffff,
        3 => 0xff00ffff,
    };

    my $surface = SDLx::Surface->new( width => 256, height => 256 );
    SDL::Video::lock_surface( $surface );

    my ( $line, $tile_counter, $base ) = ( 0, 0, 0 );
    for( my $byte = 0x0000; $byte < 0x2000; $byte++ ) {
        my $entry = {
            l => $self->read_vram( $byte ),
            h => $self->read_vram( ($byte + 8) & 0xffff ),
        };
        
        for( my $bit = 7, my $x = 0; $bit >= 0; $bit--, $x++ ) {
            my $index = ( (((($entry->{h} >> $bit) & 0x0001) << 1) | (($entry->{l} >> $bit) & 0x0001)) & 0x0003 );
            $surface->[ $x + $tile_counter * 8 ][ $line ] = $palette->{ $index };
        }

        $line++;
        if( ($line > 0) && ($line % 8 == 0) ) {
            $tile_counter++;
            $byte += 8;
            if( $tile_counter == 16 ) {
                $tile_counter = 0;
                $base += 8;
            }
            $line = $base;
        }
    }

    SDL::Video::unlock_surface( $surface );
    
    my $screen = $self->aquire_screen;
    SDL::Video::MUSTLOCK( $screen );
    $surface->blit( $screen );
    SDL::Video::unlock_surface( $screen );
    $screen->update;
}

sub dump_pattern_tables_ascii_mode {
    my $self = shift;

    my $pt0 = { start => 0x0000, end => 0x1000 };

    my $output       = [];
    my $line         = 0;
    my $tile_counter = 0;
    my $base         = 0;
    for( my $byte = $pt0->{start}; $byte < $pt0->{end}; $byte++ ) {
        my $entry = {
            l => $self->read_vram( $byte ),
            h => $self->read_vram( ($byte + 8) & 0xffff ),
        };
        
        for( my $bit = 7; $bit >= 0; $bit-- ) {
            my $color = ( (((($entry->{h} >> $bit) & 0x0001) << 1) | (($entry->{l} >> $bit) & 0x0001)) & 0x0003 );
            if( length($output->[$line]) > 0 ) {
                $output->[$line] .= $color;
            } else {
                $output->[$line] = $color;
            }
        }

        $line++;
        if( ($line > 0) && ($line % 8 == 0) ) {
            $tile_counter++;
            if( $tile_counter == 16 ) {
                $tile_counter = 0;
                $base += 8;
            }
            $line = $base;
        }
    }

    use Data::Dumper;
    open my $f, '>>', 'patterns.txt';
    print $f $_.$/ for( @$output );
    print $f $/.$/;
    close $f;
}

1;